<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>豆包TTS语音合成测试</title>
  <style>
    body { font-family: sans-serif; padding: 20px; }
    textarea { width: 100%; height: 80px; margin-bottom: 10px; }
    button { padding: 8px 16px; }
    .container {
      max-width: 800px;
      margin: 0 auto;
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }
    .info {
      background-color: #d1ecf1;
      border: 1px solid #bee5eb;
      border-radius: 4px;
      padding: 10px;
      margin: 10px 0;
    }
    .status {
      padding: 10px;
      margin: 10px 0;
      border-radius: 4px;
      font-weight: bold;
    }
    .status.connected {
      background-color: #d4edda;
      color: #155724;
      border: 1px solid #c3e6cb;
    }
    .status.disconnected {
      background-color: #f8d7da;
      color: #721c24;
      border: 1px solid #f5c6cb;
    }
    .status.playing {
      background-color: #fff3cd;
      color: #856404;
      border: 1px solid #ffeaa7;
    }
  </style>
</head>
<body>
  <div class="container">
    <h2>豆包TTS语音合成测试</h2>
    
    <div class="info">
      <strong>测试说明：</strong><br>
      - 采样率：8kHz<br>
      - 格式：PCM原始数据<br>
      - 音色：zh_male_aojiaobazong_emo_v2_mars_bigtts<br>
      - 协议：WebSocket二进制协议
    </div>

    <textarea id="text" placeholder="请输入要合成的文本"></textarea><br>
    <button onclick="sendText()">开始合成</button>
    <button onclick="stopPlay()">停止播放</button>
    <button onclick="toggleEndian()">切换字节序</button>
    <button onclick="toggleSampleRate()">切换采样率</button>
    <p id="status" class="status disconnected">状态：未连接</p>
    <p id="endian-status">字节序：小端序 (little-endian)</p>
    <p id="sample-rate-status">采样率：8kHz</p>

    <div>
      <strong>调试日志：</strong>
      <div id="log" style="background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; padding: 15px; height: 200px; overflow-y: auto; font-family: monospace; font-size: 12px;"></div>
    </div>
  </div>

  <script>
    let ws;
    let audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 8000 });
    let currentSource = null;
    let playing = false;
    let connected = false;
    let audioQueue = [];
    let isPlaying = false;
    let useLittleEndian = true; // 默认使用小端序
    let useSampleRate = 8000; // 默认使用8kHz

    function log(message) {
      const timestamp = new Date().toLocaleTimeString();
      const logDiv = document.getElementById("log");
      logDiv.textContent += `[${timestamp}] ${message}\n`;
      logDiv.scrollTop = logDiv.scrollHeight;
    }

    function updateStatus(message, className) {
      const statusDiv = document.getElementById("status");
      statusDiv.textContent = `状态：${message}`;
      statusDiv.className = `status ${className}`;
    }

    function updateEndianStatus() {
      const endianDiv = document.getElementById("endian-status");
      const endianText = useLittleEndian ? "小端序 (little-endian)" : "大端序 (big-endian)";
      endianDiv.textContent = `字节序：${endianText}`;
    }

    function updateSampleRateStatus() {
      const sampleRateDiv = document.getElementById("sample-rate-status");
      sampleRateDiv.textContent = `采样率：${useSampleRate}Hz`;
    }

    function initWebSocket(callback) {
      ws = new WebSocket("ws://127.0.0.1:8000/ws/doubao_tts");
      ws.binaryType = "arraybuffer";

      ws.onopen = () => {
        connected = true;
        updateStatus("已连接", "connected");
        log("WebSocket连接已建立");
        if (callback) callback();
      };

      ws.onmessage = (event) => {
        if (typeof event.data === "string" && event.data === "[END]") {
          updateStatus("播放完成", "connected");
          playing = false;
          log("合成完成");
          return;
        }

        if (event.data instanceof ArrayBuffer) {
          // 将音频数据加入队列
          audioQueue.push(event.data);
          log(`收到音频数据: ${event.data.byteLength} 字节`);
          
          // 调试：显示前几个字节的十六进制
          const view = new Uint8Array(event.data);
          const hexPreview = Array.from(view.slice(0, 16)).map(b => b.toString(16).padStart(2, '0')).join(' ');
          log(`音频数据预览: ${hexPreview}...`);
          
          // 如果当前没有播放，开始播放
          if (!isPlaying) {
            playNextAudio();
          }
        }
      };

      ws.onerror = (e) => {
        console.error("WebSocket 错误:", e);
        updateStatus("连接失败", "disconnected");
        connected = false;
        log("WebSocket连接失败");
      };

      ws.onclose = () => {
        console.log("WebSocket 已关闭");
        connected = false;
        updateStatus("连接已断开", "disconnected");
        log("WebSocket连接已关闭");
      };
    }

    function playNextAudio() {
      if (audioQueue.length === 0) {
        isPlaying = false;
        return;
      }

      isPlaying = true;
      const audioData = audioQueue.shift();
      
      try {
        // 豆包TTS返回的是8kHz 16bit PCM数据
        const buffer = audioContext.createBuffer(1, audioData.byteLength / 2, useSampleRate);
        const channelData = buffer.getChannelData(0);
        const dataView = new DataView(audioData);

        // 使用当前设置的字节序读取16bit数据
        for (let i = 0; i < audioData.byteLength / 2; i++) {
          let sample = dataView.getInt16(i * 2, useLittleEndian);
          channelData[i] = sample / 32768.0; // 归一化到 [-1, 1]
        }

        const source = audioContext.createBufferSource();
        source.buffer = buffer;
        source.connect(audioContext.destination);
        
        // 播放完成后继续播放下一个音频片段
        source.onended = () => {
          playNextAudio();
        };
        
        source.start();
        currentSource = source;
        log(`播放音频段: ${audioData.byteLength} 字节, ${audioData.byteLength / 2} 采样点 (${useLittleEndian ? '小端序' : '大端序'}, ${useSampleRate}Hz)`);
      } catch (error) {
        log(`音频播放错误: ${error.message}`);
        // 即使出错也继续播放下一个
        playNextAudio();
      }
    }

    function sendText() {
      const text = document.getElementById("text").value.trim();
      if (!text) return alert("请输入文本");

      // 清空音频队列
      audioQueue = [];
      
      // 停止当前播放
      if (currentSource) {
        currentSource.stop();
        currentSource = null;
      }
      isPlaying = false;

      playing = true;
      updateStatus("正在发送并播放中...", "playing");
      log(`发送文本: ${text}`);

      if (!connected || ws.readyState !== 1) {
        initWebSocket(() => {
          ws.send(text);
        });
      } else {
        ws.send(text);
      }
    }

    function stopPlay() {
      // 清空音频队列
      audioQueue = [];
      
      if (currentSource) {
        currentSource.stop();
        currentSource = null;
      }
      isPlaying = false;
      playing = false;
      updateStatus("已停止", "disconnected");
      log("播放已停止");
    }

    function toggleEndian() {
      useLittleEndian = !useLittleEndian;
      updateEndianStatus();
      log(`切换到${useLittleEndian ? '小端序' : '大端序'}`);
    }

    function toggleSampleRate() {
      useSampleRate = useSampleRate === 8000 ? 16000 : 8000;
      updateSampleRateStatus();
      log(`切换到${useSampleRate}Hz采样率`);
    }

    // 页面加载时初始化
    window.onload = function() {
      updateEndianStatus();
      updateSampleRateStatus();
    };
  </script>
</body>
</html> 